UP | HOME

GNU Emacs Lisp 学习笔记

目录

list处理

List 是 lisp 的基础。从名字应该就可以知道了,LISP(LISt Processing)

LISP list

一个 list 看起来像这样子:

'(rose violet daisy buttercup)

它是通过单引号开头的。它也可以写成下面这样子:

'(rose
violet
daisy
buttercup
)

list的元素是4种不同的花名,每个通过空格来分隔并且用括号包着。就像有个石头墙包围着它们一样。

数字,list中的list

list 中也可以有数字,例如这个 list :

(+ 2 2 )

Lisp 中,数据和程序是以相同的方式表示的,也就是说,它们都是 list 中的 word , numbers 或其他的 list ,通过空格分隔并且用括号包着。

list中的list:

'(this list has (a list inside of it ))

Lisp Atom

在 Lisp 中,我们称 wordsatomsatom 的意思是不可分割的。对 Lisp 而言, words 是用在 list 中的,并且是不可以被再分割为其他再小部分的仍然是作为程序的一部分的。

在一个 list 中, atoms 之间是通过空格来分隔的。

从技术上讲,一个 list 是由括号包围着由空格分隔的 atoms 或由其他的 listatoms 和其他 list 一起组成的。一个 list 可以仅有一个 atom 或什么都没有。一个什么也没有的 list 看起来像这样子: () ,它称为空 list. 不同于其他,一个空 list 是可以同时看作是一个 atom 和 一个 list 的。

以人类可读的文本形式表达半结构化数据的(原文为 The printed representation of ,我采用维基百科的定义来意译) 1 atomlist 都被称为 symbolic expressions 或更简洁地称为 S-expressions . 单词 expression 它自身可以指人类可读的文本形式,或者指 atom 又或者是 list . 通常是使用 expression 来统称它们(在文本上,*form* 是 expression 的同义词)

Atom 的类型
numbers
1,2,3,4,5
symbols
'+', 'foor', '-'
string
"hello world"

在 Lisp 中,所有引号的文本包括标点符号和空格是一个 atom ,这种 atom 称为 string . string 是不同于 numberssymbolsatom ,并且与它们的使用方式也不同。

list中的空格

对于 Lisp 语言来说,在 list 中的空格的个数是无关紧要的(但 atom 之间,至少要有一个空格)。比如:

'(this list
looks like this)

与下面的是一样的:

'(this list looks like this)

额外的空格和换行,只是设计于对人类更可读而已。

GNU Emacs 帮你输入 list

TAB
自动帮你缩进
代码块的缩进
M-c-\

执行一个程序

在 Lisp 中的 list 或者任何的 list ,都是一个可以准备好执行的程序。计算机将做以下三件事之一:

  • 仅返回 list 自身
  • 给出一个错误
  • 将第一个 symbol 作为命令来做一些事件(通常这个是你真正想要的)

单撇号, ' 表示一个引用。

  • 当它在一个 list 前面时,它告诉 Lisp 不要处理那个 list ,仅仅只是当作 list 自身。
  • 如果一个 list 前面没有这个单撇号,那 list 中的第一个项是特殊的 ,它是一个命令 (在 Lisp 里,这些命令称为 functions ).比如 (+ 2 2) ,它前面没有单撇号,所以,Lisp 理解 + 是一个指令。

在 GNU Emacs 里,将光标放在下面的指令里,然后执行 C-x C-e 就表示执行当前光标所在的 lisp 代码:

(+ 2 2)

'(this is a quoted list)

产生一个错误信息

我们将去 eval 一个没有单撇号并且第一个元素也是没有意义的命令的 list ,这时就会产生一个错误信息。在下面的代码里,将光标放在那里,然后按 C-x C-e :

(this is an unquoted list

这时在 *Bracktrace* 窗口将会打开并且你会看到如下的信息:

---------- Buffer: *Backtrace* ----------
Debugger entered--Lisp error: (void-function this)
(this is an unquoted list)
eval((this is an unquoted list))
eval-last-sexp-1(nil)
eval-last-sexp(nil)
call-interactively(eval-last-sexp)
---------- Buffer: *Backtrace* ----------

在该窗口中,按 q 即可退出。

sexp 它是 symbol expression 的缩写

Debugger entered–Lisp error: (void-function this) 表示没有 this 这个函数,上面说了,如果一个list开头没有单撇号,那么 Lisp 会将第一个元素当作是命令(或称为 function)来处理。

从技术上讲,*symbol* 告诉计算机去查找指令。

Symbol 名字 和 Function 定义

symbol ,可能是临时地,是定义的地方或者是指令集。与人们起名字类似。 在 Lisp 中,同一个指令集可以附加在几个不同的名字上。比如两个数相加的指令集,它被同时链接到 symbol 中的 plus+ .

在另一方面,一个 symbol 在同一时间,仅仅只有一个函数定义附于它。否则,计算机不知道到底使用哪一个定义。

由于 Emacs Lisp 是非常大的,习惯上命名 symbol 的方式是标识这些 function 是属于 Emacs 哪部分的。因此,所有处理 Textinfo 的函数都以 textinfo- 开头来命名。

Lisp 解析器

解析器所做的事:

  1. 首先查看 list 开头是否有一个引号。
  2. 如果有,则将这个 list 返回给我们
  3. 如果没有这个引号,解析器则查找 list 中的第一个元素,并且查看是否有一个与之相关联的 function 定义。如果有该函数的定义的话,则执行这些指令;否则打印出错误信息。

这就是 Lisp 的工作方式。

复杂性

第一个复杂性

除了 list, Lisp 解析器可以 eval 一个没有引号并且没有括号包围的 symbol 。 Lisp 解析器将试图把 symbol 的值作为 variable

第二个复杂性

因为一些 function 是不同寻常的并且在通常方式下是没有生效的。它们不被称为特殊的 forms ,它们用于特别的工作,例如: 定义一个 function 有一些特殊的 forms ,称为 macros ,它是 eval 为替换原 expression 的,而 function 是将一个 expression 转换为另一个 expression

介绍这些的目的,你不需要太担心哪些是特殊的 form ,哪些是 macro 或者 function 例如 if 是一个特殊的 form ,但 when 是一个 macro 。在早期的 Emacs 版本中, defun 是一个特殊的 form ,但现在它是一个 macro .

最后一个复杂性
  1. 如果 Lisp 解析器查找的 function 并不是一个特殊的 form 并且它是 list 的一部分,Lisp 解析器会查看它是否包含了另一个 list . (如此递归下去,它总是首先处理最里面的那个 list)。
  2. 否则,解析器会从左到右工作,从一个 expression 到另一个 expression

字节编译

Lisp 解析器可以解析两种形式的代码:

  1. 人类可读的代码(通常以 .el 结尾)
  2. 字节编译的代码 (byte compiled code),它并不是人类可读的,但它执行的速度比上一种更快。(通常以 .elc 结尾)

你可以将第一种人类可读的代码,转换为字节编译的代码,通过执行类似 byte-compile-file 的编译命令来转换。 你可以在 emacs/lisp (即emacs安装目录下的 lisp 目录)看到以上两种代码。

Evaluation

当 Lisp 解析器在一个 expression 上工作时,这些活动的术语就称为 evaluation 。 这个词的意思是 评估它,以查明值或数量

解析器是如何工作的

在 evaluation 一个 expression 后,Lisp 解析器将最可能返回执行完这些指令的结果值或产生一个错误消息。 在解析器返回一个值的同时,它也可能做一些其他的事,比如移动光标或复制一个文件等等,这些动作就称为 side effect .

总结: evaluation 一个 symbol expression 最常见的是导致 Lisp 解析器返回一个值以及可能执行一个 side effect; 或者产生一个错误。

evaluating 内部 list

evaluation 应用到一个 list 包含另一个 list 的 list 时,外部的 list 可能使用 第一个 evaluation 返回的值作为外部的 list 的信息。这就解析了,为什么内部的 expression 会首先被 evaluation ,它们返回的值会被外部的 expression 使用。请将光标放到下面的 expression 中,然后按 C-x C-e :

(+ 2 (+ 3 3))

执行完后,数字8将会在 echo area 中出现。

Lisp 解析器执行的情况是首先 evaluation 内部 expression , (+ 3 3) ,它返回的值是数字6;然后 evaluation 外部的 expression ,它将被重写为 (+ 2 6) ,然后返回的值是数字8 。由于这里没有更多 expressionevaluation 了,解析器就会将这值显示在 echo area 里。

C-x C-e 绑定的命令是 eval-last-sexp ,这个命令的意思是 evaluates the last symbol expression 。注意,如果你将光标放在最开始的位置或最后一个括号,执行这条命令时,返回的是8. 但如果你的光标是放在倒数第二个括号后面时,再执行这命令,它返回的是6.

Variables 变量

在 Emacs Lisp 中, 一个 symbol 可以有一个 value 附于它,就像有一个 function 定义附于它一样。它们是不同的, function 定义是一个指令集;一个 value ,是一些东西,例如是 numbername ,它们是多样的,这就是为什么叫它做 variable 的原因。

symbolvalue 可以是任何 Lisp 的 expression ,比如 number , liststring . 一个 symbol 有一个 value 通常称为 variable .

一个 symbol 可以同时拥有 function 定义和 value 附于它,又或者是它们之一。这有点类似 Cambridge 可以指 Massachusetts 的一个城市,并且也带有一些附加信息到这个名字上,例如是伟大的编程中心。 另一种思考的方式是,想像 symbol 是一堆抽屉, function 定义放在其中一个抽屉中,而 value 放在另一个抽屉,以此类推。

fill-column,一个示例 Variable

为了找出该 symbolvalue , 可以 evaluate 它自身即可。即,将光标放在该 symbol 上,然后输入 C-x C-e :

fill-column

这时可以看到 echo area 显示出一个数值(我这里是80)

一个 symbol 可以有任意的值附于它,用术语来说就是,我们 bind 一个 variable 到一个 value : 可以是 number ,例如 72; 可以是 string ,例如 "such as this";可以是 list ,例如 (spruce pine oak) ;我们甚至可以 bind 一个 function 定义给它。

一个没有 Function 的 Symbol 错误消息

当我们 evaluated fill-column 查看它的 value 时,是把它作为一个 variable ,我们没有用括号包着它。这是因为我们不想把它当作是一个 function 名。 如果 fill-column 是 lisp 中的第一个或 list 中仅有一个元素时, Lisp 解析器会试图查找附于它的 function 定义。但 fill-column 并没有 function 定义,试图 evaluating 下面时:

(fill-column)

会导致产生一个 Backtrace buffer,如:

---------- Buffer: *Backtrace* ----------
Debugger entered--Lisp error: (void-function fill-column)
(fill-column)
eval((fill-column))
eval-last-sexp-1(nil)
eval-last-sexp(nil)
call-interactively(eval-last-sexp)
---------- Buffer: *Backtrace* ----------

一个没有 Value 的 Symbol 错误消息

当我们 evaluate 一个没有 value 附加于它的 symbol 时,你将会收到一个错误消息。比如下面的 expression :

(+ 2 2)

将你的光标放到 + 号后面,第一个数字2前面,然后 C-x C-e

这时,在 Backtrace buffer 中显示 :

---------- Buffer: *Backtrace* ----------
Debugger entered--Lisp error: (void-variable +)
eval(+)
eval-last-sexp-1(nil)
eval-last-sexp(nil)
call-interactively(eval-last-sexp)
---------- Buffer: *Backtrace* ----------

在这个例子里,我们试图将 + 当作一个 variableevaluate 它的 value 而不是 function 定义 ,但并没有找到该 value ,所以就报了该错误消息。

Arguments 参数

下面的例子:

(+ 2 2)

Lisp 解析器做的是,将 + 后面的 number 进行相加。这些通过 + 相加的 number 称为 functionarguments 在 Lisp 里,*function* 的 argumentsatomslist .

Arguments 的数据类型

传递什么样的数据类型,取决于一个 function 用它们来做什么。 string 的字符是从 0 开始算起的,而不是 1.

作为 Value 或 List 的 Argument

一个 argument 可以是一个 symbol 进行 evaluate 之后返回的 value 。例如,*fill-column* 进行 evaluated 时,它返回一个 number ,这个 number 可以用在加法中。例如下面:

(+ 2 fill-column)

此外,一个 argument 也可以是一个 list 进行 evaluated 之后返回的 value .例如下面的 expression :

(concat "The " (number-to-string (+ 2 fill-column)) " red foxes.")

可变数量的 Argument

一些 function ,例如 concat , +* ,可以带有任意数量的 arguments 例如下面的:

(+) ;;结果为0
(*) ;;结果为1


(+ 3) ;;结果为3
(* 3) ;;结果为3

(+ 3 4 5) ;;结果为12
(* 3 4 5) ;;结果为60

使用一个错误类型的对象作为 Argument

当传递一个错误类型的 argument 给一个 function 时, Lisp 解析器会产生一个错误消息。例如 , + 函数期待它们的 arguments 都是 numbers . 作为一个例子,我们传递一个引号的 symbol hello 来代替一个 number 。然后 C-x C-e :

(+ 2 'hello)

执行完后,将会产生一个错误消息。在 Backtrace buffer 中会显示:

---------- Buffer: *Backtrace* ----------
Debugger entered--Lisp error:
(wrong-type-argument number-or-marker-p hello)
+(2 hello)
eval((+ 2 (quote hello)))
eval-last-sexp-1(nil)
eval-last-sexp(nil)
call-interactively(eval-last-sexp)
---------- Buffer: *Backtrace* ----------

错误消息的第一部分是比较直接的,它说 wrong type argument number-or-marker-p 表示 + 期待的 argument 类型,这个 symbol 告诉我们,它期待的 Argument 是一个 number 或一个 marker (它是一个特殊的对象,表示 buffer 的 position).

在 Emacs 中, buffer 中的 locations 是作为 markers 来记录的,*marker* 可以看作是一个 number*,表示从 buffer 的开始位置到该 *marker 的字符数。

number-or-marker-p 中的 p 表示 predicate (谓词),在早期 Lisp 研究员中使用的术语,它指的是一个 function 决定一些 propertytrue 还是 false .

所以,*p* 告诉我们, number-or-marker-p 是一个判断 arguments 是否是一个 numbermarkertrue 还是 falsefunction 名字。

最后一部分的错误消息是 symbol hello .

message Function

一个 message 是打印在 echo area 的。例如:

(message "This message appears in the echo area!")

如果双引号里的字符中有 %s ,那么 message function 不打直接打印出来,而是查找该双引号字符串后面的 argument ,它会 evaluate 第二个参数,然后将 value 替换到 %s 所在的位置。例如下面:

(message "The name of this buffer is : %s" (buffer-name))

如果这里有多个 %s ,它会依此类推下去。例如下面:

(message "There are %d %s in the office!"
     (- fill-column 14) "pink elephants")

设置一个 Variable 的 Value

方式:

  1. 使用 set function
  2. 使用 setq function
  3. 使用 let

使用 set

例如下面:

(set 'flowers '(rose violet daisy buttercup))

evaluate 上面的 expressionC-x C-e )可以看到 (rose violet daisy buttercup)echo area 中显示。作为一个 side effect ,*flowers* symbol 现在是绑定了 list,这时 flowers 可以被视作一个 variable 了。它的值是后面的 list. 设置完之后,你可以 evaluate 这个 symbol flowers :例如:

flowers

另外,如果你 evaluate 的是前面带有一个引号的 variable'flowers*,在 *echo area 中你会看到该 symbol 自身。 也要注意,当你使用 set 时,你需要将所有的 arguments 都要带有引号,除非它想它们进行 evaluated .

set 的第一个参数没有带有引号时,那么第一个参数将首先进行 evaluated 后再继续,如果这时它并没有一个 value 绑定到 flowers ,这时就会产生一个错误消息: "Symbol's value as variable is void"。 如果这时 evaluated 后, 它是有一个 value 的,那么 set 将调图设置的是这个返回的 value

使用 setq

实际情况下,你几乎总是将 set 的第一个参数用引号引起来的。组合 set 和第一个参数使用引号的情况是如此常见,所以,它有一个自己的特别 form 为:*setq* 。即:*setq* == set + quote 。所以下面是等价的:

(setq carnivores '(lion tiger leopard))

(set 'carnivores '(lion tiger leopard))

并且 setq 可以同时进行多个绑定,例如:

(setq trees '(pine fir oak maple)
  herbivores '(gazelle antelope zebra))

统计 Counting

(setq counter 0)                ; Let’s call this the initializer.

(setq counter (+ counter 1))    ; This is the incrementer.

counter                         ; This is the counter.

第一个 expression 表示初始化一个 variablecounter 并且它的 value 为 0。 第二个 expression 表示进行累加,每执行一次,都会在上一次的基础上累加1 第三个 expression 显示该 variable counter 的值

总结

  • Lisp 程序是由 expression 组成的,它们是 listatoms
  • List 是由0个或多个 atoms 或内部 list 组成的,通过空格分隔并且用括号包着的。一个 list 可以为空。
  • Atoms 是多个字符的 symbol ,例如: forward-paragraph ;或单个字符的 symbol ,例如: + ;或是在双引号之间字符串的字符,如 "hello world";或者是 numbers ,如:1,2,3。
  • 一个 number evaluate 后是它自身
  • 一个由双引号包着的字符串进行 evaluate 后,也是它自身
  • 当你进行 symbol 的 evaluate 后,它会返回它的 value
  • 当你 evaluate 一个 list 时, Lisp解析器会查找 list 中的第一个 symbol ,然后查找该 symbol 所绑定到的 function 定义。然后执行这些 function 定义的指令集
  • 一个单引号 ' 告诉 Lisp 解析器应该返回该 expression 自身,而不是进行 evaluate
  • Arguments 是传递给 function 的信息。参数是 list 中第一个元素的 function 的其余元素。如 (+ 2 2 ) ,第一个元素为 + , 并且没有引号,那么它就会看作是 function , 后面两个2就是该 function 的参数。
  • 一个 function 进行 evaluate 后总是返回一个 value (除非它产生了一个错误)。另外,它也会执行一些动作,称为 side effect 。通常情况下,一个 function 的主要目的就是产生一个 side effect

实践 Evaluation

如何 Evaluate

无论何时给出一个编辑的命令到 Emacs Lisp ,例如这些命令:移动光标或滚动屏幕,你都是正在进行 evaluating 一个 expression ,它的第一个元素是一个 function 。这就是 Emacs 如何工作的。 当你输入按键时,你会导致 Lisp 解析器去 evaluate 一个 expression 然后你就获得你的结果。甚至输入纯文本时也需要 evaluate 一个 expression ,在这个例子里,就是 self-insert-command ,它只是简单地插入你输入的字符。 那些通过输入按键来进行 evaluatefunction 称为 interactive function 或 command.

Buffer 名字

有两个 functionbuffer-namebuffer-file-name ,区别了 filebuffer 之间的不同。 通常情况下, (buffer-name) 与文件的名字相同. (buffer-file-name) 返回的是文件的绝对路径。

filebuffer 是两个不同的实体。*file* 是计算机持久化记录的。 buffer 是 Emacs 内部的信息,它会在结束一个 session 时(或 kill 掉 buffer)而消失。 通常,一个 buffer 包含了一份已经从 filecopy 的信息了的,这时我们说,该 buffer 正访问该 file 。这份 copy 就是你工作或修改的。对 buffer 进行修改,并不会改变 file ,除非你保存该 buffer 。 当你保存 buffer 时,该 filecopy 就会持久化到 file 里。

(buffer-name)
(buffer-file-name)

nil 对应的拉丁文是 nothing 。在 Lisp中, nil 也用作 false 的意思,它的同义词是空 list,即 ()

单词 buffer ,来自生活中的减弱碰撞力的词:*cushion* (垫子)的意思。在早期的计算机中,缓冲器缓冲了文件和计算机的中央处理单元之间的交互。 保持文件的鼓或磁带和中央处理单元是彼此非常不同的设备,它们以自己的速度以喷射工作。 缓冲区使他们能够有效地协同工作。

并不所有的 buffer 都会关联 file 的。比如 scratch buffer 就没有访问任何文件。同样地, Help buffer 也没有。

  • 如果你想将 evaluate 的结果显示在 buffer 自身里面,而不是显示在 echo area ,你可以使用 C-u C-x C-e 来代替 C-x C-e

获取 Buffer

为了获取 buffer 自身而不是 buffer 的名字,可以使用 current-buffer function 。 但是,如果你 evaluateexpression : (current-buffer) ,你会看到,它也是打印该 buffer 的名字在 echo area 中。 Emacs 这样子做的原因有2:

  1. buffer 可能有成千上万行,太长的话,不方便显示
  2. 可能有相同的内容,但不同的名字。区别它们是非常重要的。
(current-buffer)#<buffer GNU emacs lisp学习笔记.org>
(buffer-name)"GNU emacs lisp学习笔记.org"
(other-buffer)#<buffer org-blog>

可以看到,如果是 (current-buffer) 返回的话,它显示的是 #<buffer GNU emacs lisp学习笔记.org>(buffer-name) 返回的是 GNU emacs lisp学习笔记.org

另一个相关的 function(other-buffer) 。它返回的是最近选择过的非当前的 buffer .

切换 Buffer

other-buffer function 可以用在一个 function 中需要 buffer 作为参数时,提供一个 buffer 。我们可以看到使用 other-bufferswitch-to-buffer 来进行切换 buffer . 通常按 C-x b 然后在提示框中输入 scratch 来切换 buffer 。*C-x b* 会导致 Lisp 解析器去 evaluate 一个 interactive function : switch-to-buffer 。正如之前说过,这就是 Emacs 的工作方式,不同的按键会调用或执行不同的 function 。例如 C-f 调用 forward-charM-e 调用 forward-sentence 等等

下面是一个进行切换的例子:

(switch-to-buffer (other-buffer))

evaluate 上面的例子,就可发现 emacs 进行了切换 buffer 了。

Buffer 大小 和 位置点

buffer-size function ,它告诉你当前 buffer 的大小。它返回的是 buffer 当字符的总数。

在 Emacs 中,光标的当前位置称为 point 。*(point)* 返回的是从当前 buffer 中距离整个 buffer 最开始的位置到当前光标所在的位置的字符数。

point-minipoint 类似,但是,它返回的是当前 buffer 允许的最小的 point 值。它的值是1,除非 narrowing 是生效的。(narrowing,它是一种机制,允许你限制自己或程序,仅仅能操作 buffer 其中的一部分)

point-maxpoint-min 类似,只是它返回的是最大的 point 值。

(buffer-size)16365
(point)16363
(point-min)1
(point-max)16716

如何写 Function 定义

原生函数

它们是用 C 语言写的函数。

重新强调一次:当你用 Emacs Lisp 写代码时,你不用区分这些 function 是用 C 语言写的,还是用 Emacs Lisp 写的。这些差别是无关紧要的。

The defun Macro

在 Lisp 中,比如 mark-whole-buffer 有附于它的代码,它告诉计算机当调用该 function 时要做的是什么。这些代码,就称为 function 定义,它是通过 evaluate 一个 Lisp 的以 defun (它是 define function 的缩写) 开始的 symbolexpression 来创建的。

function 的定义在单词 defun 后面有以下5部分:

  1. symbolname
  2. 一个 argument 列表。如果不需要的话,这部分为空list,即: ()
  3. 描述该 function 的文档(技术上是可选的,但强烈建议写)
  4. 可选的。一个 expression ,它使该 function 变成一个 interactive function ,因此,你可以通过 M-x 来调用该 function 或输入适合的按键来调用。
  5. 计算机要进行操作的指令代码,即该 functionbody

下面是一个伪代码:

(defun function-name (arguments…)
"optional-documentation…" ;; 文档会在 C-h f 查看时显示
(interactive argument-passing-info)     ; optional
body…)

下面是一个具体的例子,该 function 是将 argument 乘以 7 :

(defun multiply-by-seven (number)
"Multiply NUMBER by seven."
(* 7 number)
)

下面展示了如何使用上面例子的 function ,但是不要尝试进行 evaluate 它!

(multiply-by-seven 3)

注意,传递 argument 时,是不需要括号的,虽然在 function 定义时使用了括号,但调用时并不需要。定义时需要,是因为要告诉计算机 argument 从哪里开始以及结束。

安装一个 Function 定义

安装一个 function 定义,你可以在该 function 定义的最后一个括号后面,按下 C-x C-e 来进行 evaluate 它,这时你可以看到 multiply-by-seven 会出现在 echo area 里。同时,该动作会进行安装该 function 的定义了。

function 必须要先安装后才能使用。

通过这种方式安装的 function 定义,但当你退出 Emacs 时,就不会保留了。

创建一个 Interactive Function

你可以使一个 function 变成 interactive 的,通过在 function 文档后面的 list 中,以特殊的 form interactive 开始。这样子,就可以在 M-x 中调用该 function ,或者通过按下绑定到该 function 的按键来调用,像 C-n 是调用 next-lineC-x h 是调用 mark-while-buffer 一样。

当你调用一个 interactive function 时,返回的 value 并不会自动显示在 echo area 。这是因为你会经常调用一个 interactive function 来作为一个 side effect ,比如将光标移动到下一个单词或下一行,并没有 value 返回。如果返回的 value 每次都显示在 echo area ,这会使你分心的。

multiply-by-seven 的 interactive 版本

(defun multiply-by-seven (number)       ; Interactive version.
"Multiply NUMBER by seven."
(interactive "p")
(message "The result is %d" (* 7 number)))

安装完上面的 function 后,你就可以通过 M-x multiply-by-seven RET 来调用了。这时,你可以看到 The result is 7echo area 里了。

你可以通过以下2种方式来进行传递 argument 来调用该 function :

  1. 先输入 argument 前缀,然后再通过 M-x 来调用。例如:*C-u 3 M-x multiply-by-seven* 或者
  2. 输入绑定到该函数的按键,假设它绑定到 M-e ,这时可以这样子调用: C-u 3 M-e

multiply-by-seven 细节

(defun multiply-by-seven (number)       ; Interactive version.
"Multiply NUMBER by seven."
(interactive "p")
(message "The result is %d" (* 7 number)))

(interactive "p") 是一个带有2个元素的 list 。"p" 告诉 Emacs 传递前缀 argument 给该 function 并且使用它的 value 作为 functionargument

interactive 的不同选项

思考一下 zap-to-char function,它的 interactive 部分的 expression 为:

(interactive "p\ncZap to char: ")
"p"
argument 告诉 Emacs 解析它个前缀 argument ,并作为一个 number 传递给该 function
"c"
告诉该 function 要删除的字符的名字

更多的形式地,它可以传递多个这些选项给 interactive ,每一个之间,是通过 \n 来分隔的。

持久化地安装 Code

当你是通过 evaluate 来安装 function 时,它会一直保留直到退出 Emacs 。下次打开 Emacs 时,该 function 并不会被安装,再到再次进行 evaluate 当你想要在启动 Emacs 时,自动安装这些 function ,你可以有以下几种方式来达到目的

  • 如果你的这些 code 仅是为你自己工作的,你可以将它们放到 .emacs 初始化文件中。该文件会在 Emacs 启动时,自动地进行 evaluate
  • 或者,你可以将这些 function 定义放到一个或多个文件中,然后使用 load function 来使 Emacs 进行 evaluate
  • 如果你想你的代码,在整个 site 中都可用,你可以将它们放到一个名为 site-init.el 的文件中,该文件会在 Emacs 构建时加载。这可使这些 code 在你的计算机中每个人都是可用的。

let

let expression 是一个特别的 form ,你将会在 function 定义时经常使用它。

let 用于附加或绑定一个*value* 到一个 symbol 中。

理解它是必须的。考虑一下以下这情况,你有一个自己的家,在语句 The house needs painting ,你通常用 the house 来引用它。如果你正在你的一个朋友的家中进行拜访,然后你再次说 "the house* ,他会认为正在引用的是你朋友的 house ,而不是你自己的 house .

let 防止混淆

let 会创建一个 local variable 无论在 let 外部的 expression 是否有相同的名字的 variable ,它都可以让内部的 expression 通过 let expression* 创建的 local variable 仅仅在 let expression 自身和调用 letexpression 内保留这些 variable ,在这之外,*let* expression 是其他地方是不生效的。 用计算机术语来说就是,绑定的 symbol 是仅仅在 function 中调用 letform 才是可见的,在此之外是不可见的。

let 可以一次性创建多个 variable 。*let* 会为每个 variable 创建一个初始 value ,该 value 可以是你指定的,或者是 nil

let 创建和绑定 variable 后,它就会 execute 这些 code ,然后返回 let body 中的 最后一个 expression 的 value 作为整个 let expression 的 value .

let expression 部分

let expression 有3部分:

  1. 第一部分是 symbol let
  2. 第二部分是一个 list, 称为 varlist ,每个元素是一个 symbol 自身或者是一个2元素的 list, list中的第一个元素是一个 symbol
  3. 第三部分是 let expression 的 body,通常由一个或多个 list 组成

一个 let expression 的模板看起来像这样子:

(let varlist body…)

varlist 中的 variable symbol 是由 let 进行初始化值的。*symbol* 自身的话,就会初始化为 nil ;如果是一个2元素的 list,它就会将第一个元素作为 symbol ,然后将 evaluate 第二个元素返回的 value 绑定到第一个元素上。

因此,一个 varlist 看起来像这样子: (thread (needles 3)) 。在这个例子里,*thread* 会被 let 初始化为 nil , 并且 needles 被初始化为3.

let expression 例子

(let ((zebra "stripes")
  (tiger "fierce"))
  (message "One kind of animal has %s and another is %s."
       zebra tiger)) "One kind of animal has stripes and another is fierce."

let 语句中没有初始化的 variable

(let ((birch 3)
  pine
  fir
  (oak 'some))
  (message
  "Here are %d variables with %s, %s, and %s value."
  birch pine fir oak)) "Here are 3 variables with nil, nil, and some value."

特殊的 form : if

除了 defunlet 之外,第三个特殊的 form 是条件 if .它是用于训练计算机进行决定的。 if 的基本思想是, if 测试为 true ,则 evaluate then 的 expression 。如果测试为 not true ,则不进行 evaluate

if 的更多细节

if expression,第一个元素为 if ,第二个元素为 test ,第三个元素为 action 。尽管如此,*test* 部分经常被称为 if-part ,*action* 部分经常被称为 then-part 下面是伪代码:

(if true-or-false-test
action-to-carry-out-if-test-is-true)

下面是具体的例子:

(if (> 5 4)
    (message "5 is greater than 4!")
    )

下面是另一个例子:

(defun type-of-animal (characteristic)
"Print message in echo area depending on CHARACTERISTIC.
If the CHARACTERISTIC is the string \"fierce\",
then warn of a tiger."
(if (equal characteristic "fierce")
  (message "It is a tiger!")))

安装完上面的 function ,就可以执行下面的例子了:

(type-of-animal "fierce")  
(type-of-animal "striped")

type-of-animal 细节

function 的名为 type-of-animal ,并且接受一个 argument 。*function* 模板为

(defun name-of-function (argument-list)
"documentation…"
body…)
(defun type-of-animal (characteristic)
"Print message in echo area depending on CHARACTERISTIC.
If the CHARACTERISTIC is the string \"fierce\",
then warn of a tiger."
body: the if expression)

if 模板为 :

if true-or-false-test
    action-to-carry-out-if-the-test-returns-true)

所以,在 type-of-animal 里, if 的代码如下:

(if (equal characteristic "fierce")
(message "It is a tiger!")))

if-then-else

if expression 有一个可选的第三部分,称为 else-part ,它是在 test-partfalse 时才调用的。下面是伪代码:

(if true-or-false-test
    action-to-carry-out-if-the-test-returns-true
  action-to-carry-out-if-the-test-returns-false)

具体例子:

 (if (> 4 5)                               ; if-part
  (message "4 falsely greater than 5!") ; then-part
(message "4 is not greater than 5!"))   ; else-part

type-of-animal 添加 else-part

(defun type-of-animal (characteristic)  ; Second version.
"Print message in echo area depending on CHARACTERISTIC.
If the CHARACTERISTIC is the string \"fierce\",
then warn of a tiger; else say it is not fierce."
(if (equal characteristic "fierce")
   (message "It is a tiger!")
 (message "It is not fierce!")))

Emacs Lisp 中的 true 和 false

falsenil (空 list 也是 nil 的一种) 在 test-part 里,都是 false 。除此之外,其他的都是 true

nil 解释

在 Emacs Lisp 中 , nil 有两种意思:

  1. 表示 空 list ,即 ()
  2. test-part 中,表示 false

除了 not nilvalue 之外,其他都被视作为 true 。下面是一些例子:

(if 4
    'true
 'false)true

 (if nil
       'true
   'false)false

save-excursion

save-excursion ,它用于保存 point 的位置,*function* 的执行 body ,和 当它的位置变了的时候,可以恢复该 point 之前的位置。(即执行完 body 后,又将光标恢复到之前的位置)

point 和 mark

point 指的是当前光标的位置。无论光标在哪里,哪里就称为 point 。在 Emacs Lisp 中, point 是一个整数,在 buffer 中第一个字符是数字1,第二个字符是数字2等等。*point* function 返回的是光标的当前位置距离最开始的第一个字符的字符数。

markbuffer 中的另一个位置。它的 value 可以通过 C-SPC (set-mark-command)命令来设置。

如果你设置了一个 mark ,你可以使用命令 C-x C-x (exchange-point-and-mark)来使用光标跳转到 mark 的位置并且设置 mark 为光标的之前一个位置。 另外,如果你设置了另一个 mark ,则之前的 mark 会保存到 mark ring 中。(M-x helm-mark-ring)

region : 在 buffer 中的 pointmark 之间的部分,就称为 region .这么多命令是在 region 上工作的,包括: center-region, count-line-region, kill-regionprint-region

save-excursion 模板

(save-excursion
body…)

body 部分是一个或多个 expression ,它们将会被 Lisp 解析器按顺序地进行 evaluated 。如果有多个 expression ,则最后一个 expressionvalue 将会作为整个 save-excursion function 的返回值。 其他部分的 expression 仅进行 evaluate 来产生 side effect

更详细的模板如下:

(save-excursion
first-expression-in-body
second-expression-in-body
third-expression-in-body
…
last-expression-in-body)

通常,它会与 let 一起工作:

(let varlist
(save-excursion
body…))

回顾

  • eval-last-sexp
  • defun
  • interactive ,它的常用选项(选项之间通过 \n 来分隔)有:
    b
    一个存在的 buffer 名字
    f
    一个存在的 file 名字
    p
    前缀的数字 argument (它是小写的 "p")
    r
    pointmark , 作为2个数字 argument ,最小的在最开头。
  • let
  • save-excursion
  • if
    • <
    • >
    • <=
    • >=
    • =
    • equal
    • eq
    • string<
    • string-lessp
    • string=
    • string-equal
  • message
  • message
  • setq
  • set
  • buffer-name
  • buffer-file-name
  • current-buffer
  • other-buffer
  • switch-to-buffer
  • set-buffer
  • buffer-size
  • point
  • point-min
  • point-max

一些 buffer 相关的 Functions

查找更多信息

C-h f function-name RET : 查看 function-name 的详细文档

C-h v variable-name RET : 查看 variable-name 的详细说明

describe-function :查看 function 的定义

C-h p 查看 Emacs Lisp libraries

精简版的 beginning-of-buffer 定义

它将光标移动到 buffer 的开头。通常绑定到 M-< 首先,它必须是 interactive 的,这样我们才可以通过 M-x beginning-of-buffer 来调用或者使用快捷键 M-< 来调用;必须离开 buffer 中的 mark 的原始位置;并且它必须移动光标到 buffer 的开头。 下面是完整的精简版代码:

(defun simplified-beginning-of-buffer ()
"Move point to the beginning of the buffer;
leave mark at previous position."
(interactive)
(push-mark)
(goto-char (point-min)))

(push-mark) 它会将当前光标的位置设置一个 mark ,该位置会保存到 mark ring 中。这样子,当你跳转到 buffer 的开头的时候,这时再按 C-x C-x 就可以恢复原来的位置了。

以后,当你遇到没有见过的 function 时,你可以通过 describe-function 来详细了解它,或者用 C-h f

mark-whole-buffer 的定义

它通常绑定到 C-x h

mark-whole-buffer 概述

在 GNU Emacs 22 中,它的源码如下:

(defun mark-whole-buffer ()
"Put point at beginning and mark at end of buffer.
You probably should not use this function in Lisp programs;
it is usually a mistake for a Lisp function to use any subroutine
that uses or sets the mark."
(interactive)
(push-mark (point))
(push-mark (point-max) nil t)
(goto-char (poinnt-min)))

可以看到,它与上面的 simplified-beginning-of-buffer 是相似的。

mark-whole-buffer 的 body

(push-mark (point))
(push-mark (point-max) nil t)
(goto-char (point-min))

(push-mark (point)) 这一行与上面的 simplified-beginning-of-buffer 做的事情是一样的。都是将当前光标的位置设置一个 mark 。 不过 simplified-beginning-of-buffer 的是 (push-mark) 而不是 (push-mark (point)) 。 虽然不知道为什么两个不同的函数,写法不一样。可能这个写代码的人不知道 push-markargument ,当没有传递 argument 给它时,它会自动地为当前光标设置一个 mark ,又或者是为了以下面的代码保持风格。

(push-mark (point-max) nil t) ,这个 exchange 比较复杂。该 expression 与上一行的差不多一样。但这里多了2个 argument 。第二个 argumentnil ,表示当它设置了一个 mark 后,它应该显示一条消息:"Mark set"。第三个参数为 t ,它告诉 push-markTransient Mark mode 是开启的时候,激活这个 mark 。*Transient Mark mode* 会高亮当前激活的 region 。通常情况下,它是关闭的。

最后一行是 (goto-char (point-min)) ,这样子的结果就是将光标移动到 buffer 的开头,并且在 buffer 的最后设置了一个 mark ,这样子就是 mark 了整个 buffer 了。

append-to-buffer 的定义

append-to-buffer ,它做的事就是,从当前 buffer copy 一个 region (即在 pointmark 之间的部分) 到另一个指定的 buffer

append-to-buffer 概述

它是使用 insert-buffer-substring 来进行 copy region 的。正如它的名字一样,它从一个 buffer 中获取 substring ,然后插入到另一个 buffer . 下面是完整的定义:

    (defun append-to-buffer (buffer start end)
  "Append to specified buffer the text of the region.
It is inserted into that buffer before its point.

When calling from a program, give three arguments:
BUFFER (or buffer name), START and END.
START and END specify the portion of the current buffer to be copied."
  (interactive
   (list (read-buffer "Append to buffer: " (other-buffer
                                            (current-buffer) t))
         (region-beginning) (region-end)))
  (let ((oldbuf (current-buffer)))
    (save-excursion
      (let* ((append-to (get-buffer-create buffer))
             (windows (get-buffer-window-list append-to t t))
             point)
        (set-buffer append-to)
        (setq point (point))
        (barf-if-buffer-read-only)
        (insert-buffer-substring oldbuf start end)
        (dolist (window windows)
          (when (= (window-point window) point)
            (set-window-point window (point))))))))
append-to-buffer 的 Interactive 部分

代码如下:

(interactive
(list (read-buffer
   "Append to buffer: "
   (other-buffer (current-buffer) t))
  (region-beginning)
  (region-end)))

它是以一个带有3部分的 list 开始的。

  1. list 的第一部分是一个 expression ,用于读取一个 buffername ,并作为一个 string 返回。这就是 read-buffer 。该 function 需要一个提示来作为它的第一个 argument "Append to buffer: " 。第二个 argument 是告诉该 command 如果你不指定任何 value 时,就使用这个提供的 value 。 在这个例子里,使用的是一个 expression ,它包含 other-buffer ,一个 exception 和一个表示 true 的 t other-buffer 的第一个 argumentexception 部分,它是另一个 function current-buffer 。这不会返回。第二个 argumentsymbol t ,它告诉 other-buffer ,它可以显示那些可见的 buffers (除了 current-buffer ).该 expression 就是 (other-buffer (current-buffer) t)
  2. 第二部分是 region-beginning
  3. 第三部分是 region-end

刚开始时,该 command 是使用 Br 的,这样整个 interactive 部分看起来像这样子:

(interactive "BAppend to buffer: \nr")
append-to-buffer 的 Body 部分

它是以 let 开头的。它有三个元素:

  1. symbol let
  2. varlist
  3. body

在这里, varlist 看起来像这样子: (oldbuf (current-buffer)) 。在这部分中,有一个 variableoldbuf ,它绑定了 (current-buffer) 的返回 value 。它用于保持跟踪 buffer 的。

append-to-buffer 的 save-excursion 部分

save-excursion 是保存当前光标的位置 point ,当 save-excursionexpression 执行完毕后,就恢复这个 point 的位置。另外,*save-excursion* 它会保持跟踪原来的 buffer ,然后恢复它。

save-excursion 的模板如下:

(save-excursion
first-expression-in-body
second-expression-in-body
…
last-expression-in-body)

在这个例子里, save-excursion 仅有一个 expression ,=let*= expression 。注意,这个 let*let 不同的。 let* 可以让 Emacs 按顺序来设置每一个 variable

set-buffer ,在以前,*set-buffer* 是比较简单地为:

(set-buffer (get-buffer-create buffer))

但现在是

(set-buffer append-to)

append-to 在之前的 let* 已经绑定到 (get-buffer-create buffer) 了。

append-to-buffer 的定义是,从当前的 buffer 中的 region 插入到另一个指定名字的 buffer 中。 insert-buffer-substring ,它是从另一个 buffer 中 copy 文本到 当前 buffer ,只不过 append-to-buffer 是反过来了,这就是为什么在定义的开始时,让 let 绑定了一个 local symbol oldbuf 来跟踪 current-buffer

insert-buffer-substring 部分看起来是这样子:

(insert-buffer-substring oldbuf start end)

insert-buffer-substring 从它的第一个 buffer 可 copy 文本,然后插入到当场所在的 buffer 中。当 insert-buffer-substring 工作完后,*save-excursion* 就会恢复原来的 buffer ,并且 append-to-buffer 也就完成了它的工作了。

伪代码如下:

     (let (bind-oldbuf-to-value-of-current-buffer)
  (save-excursion                       ; Keep track of buffer.
    change-buffer
    insert-substring-from-oldbuf-into-buffer)

  change-back-to-original-buffer-when-finished
let-the-local-meaning-of-oldbuf-disappear-when-finished

总结一下 append-to-buffer 的工作原理:它保存当前 buffer 到一个名为 oldbuf 的 variable 中。然后获取一个新的 buffer (如果有需要的话,就创建一个)并且 Emacs 会切换到这个新的 buffer 中。使用 oldbuf 中的 value ,它从 oldbuf 中的 region 文本插入到该新的 buffer 中。最后使用 save-excursion ,它会带你回到原来的 buffer 中,(即 oldbuf ).

回顾

  • describe-function
  • describe-variable
  • find-tag
  • save-excursion
  • push-mark
  • goto-char
  • insert-buffer-substring
  • mark-whole-buffer
  • set-buffer
  • get-buffer-create
  • get-buffer

一些更复杂的 Functions

copy-to-buffer 的定义

理解了 append-to-buffer 之后,再理解 copy-to-buffer 就容易了。它 copy 文本到一个 buffer ,但它不是添加到第二个 buffer 中,而是代替第二个 buffer 的之前的所有文本。它的 body 部分看起来是这样子:

...
(interactive "BCopy to buffer: \nr")
(let ((oldbuf (current-buffer)))
  (with-current-buffer (get-buffer-create buffer)
    (barf-if-buffer-read-only)
    (erase-buffer)
    (save-excursion
      (insert-buffer-substring oldbuf start end)))))

get-buffer-create buffer 它告诉计算机使用指定名的 buffer 来准备 copy ,如果该 buffer 不存在,则创建一个。然后 with-current-buffer evaluate 它的 body 。

barf-if-buffer-read-only 如果 bufferread-only 的话,就给出一条错误信息

erase-buffer 即擦掉整个 buffer 的内容

save-excursion 上面也详细说明了。

这就是 replacement 的意思, Emacs 擦除掉原来的文本,然后插入新的文本。

copy-to-buffer 的 body 看起来像这样子:

(let (bind-oldbuf-to-value-of-current-buffer)
 (with-the-buffer-you-are-copying-to
   (but-do-not-erase-or-copy-to-a-read-only-buffer)
   (erase-buffer)
   (save-excursion
     insert-substring-from-oldbuf-into-buffer)))

insert-buffer 的定义

它做的事就是,从其他的 buffer copy 文本到当前 buffer 。它是 append-to-buffercopy-to-buffer 的相反操作,它们是从当前 buffer copy 到另一个 buffer

insert-buffer 的代码

(defun insert-buffer (buffer)
  "Insert after point the contents of BUFFER.
Puts mark after the inserted text.
BUFFER may be a buffer or a buffer name."
  (interactive "*bInsert buffer: ")
  (or (bufferp buffer)
      (setq buffer (get-buffer buffer)))
  (let (start end newmark)
    (save-excursion
      (save-excursion
        (set-buffer buffer)
        (setq start (point-min) end (point-max)))
      (insert-buffer-substring buffer start end)
      (setq newmark (point)))
    (push-mark newmark)))
insert-buffer 中的 interactive 部分

它有两部分: *bInsert buffer:

  • 一个 read-only buffer

    星号 * 是当当前 buffer 是一个 read-only 或并不能修改时出现的。如果 insert-buffer 是在当前 buffer 为 read-only 的情况下调用的,就会有 echo area 中显示一条消息并且会嘟嘟噢或闪着光标,表示不允许你插入任何东西到当前 buffer 。星号并不需要与下一个 argument 通过换行来分隔。

  • interactive 中的 b 选项

    b 告诉 Lisp 解析器, insert-bufferargument 应该是一个存在的 buffer 或者是它的名字。("B" 选项则表示该 buffer 是可能不存在的)。这时 Emacs 会等待你输入 buffer 的名字,提供一个默认的 buffer ,并可以补全名字。如果该 buffer 并不存在,你会收到一个 "No match" 的消息,然后你的 terminal 可能又会嘟嘟响。

insert-buffer 中的 body 部分

主要有2部分: or expression 和 let expression 。

or expression 的上的是确保 argument buffer ,是绑定到一个 buffer 而不仅仅是一个 buffer 的名字。

let expression 包含的代码是从其他 buffer copy 到当前 buffer 中。

在框架上,这两部分如下:

(defun insert-buffer (buffer)
"documentation…"
(interactive "*bInsert buffer: ")
(or …
…
(let (varlist)
body-of-let… )
用 if 来代替 insert-buffer 中的 or

这部分的工作,就是确保 buffervalue 是一个 buffer 自身,而不仅仅是一个 buffer 的名字。 这里,我们用 predicate (谓词): bufferp 来告诉我们,到底是 buffer 自身,还是只是名字。可以这样子写:

(if (not (bufferp buffer))              ; if-part
    (setq buffer (get-buffer buffer)))  ; then-part

not 是一个 function ,即如果 argumenttrue ,则返回 false ; 如果是 false ,则返回 true

get-buffer :通过给定的名字,返回特定的 buffer

body 中的 or 部分

上面我们用 if 来代替了 or ,但实际上,源码里是 or ,为了理解它,我们需要知道 or 是如何工作的。

or 可以有任意个 argument ,它会一直为每个 argument 进行 evaluate 直到遇到第一个非 nilargument 。(即遇到第一个非 nil 即返回,不会再继续进行后面的 evaluate 了)。

(or (bufferp buffer)
    (setq buffer (get-buffer buffer)))
insert-buffer 中的 let 部分

它有三个 local variablestart, endnewmark ,都初始为 nil 。这些 variable 用于 let 的后面部分,并且会临时隐藏在 Emacs 的其他地方出现的同名 variable ,直到 let 结尾。 它包含有两个 save-excursion ,首先我们看最里面的。它的代码如下:

(save-excursion
  (set-buffer buffer)
  (setq start (point-min) end (point-max)))

set-buffer buffer 让 Emacs 从当前 buffer 切换到另一个被 copy 文本的 buffer 中,然后在那个 buffer (即被 copy 的 buffer)中,*start* 和 end 的 variable 被设置为那个 buffer 的开始和结束位置,*point-min* 和 point-max 。注意到,*setq* 同一个 expression 中,设置了2个 variable

外部的 save-excursion 看起来像这样子:

(save-excursion
  (inner-save-excursion-expression
   (go-to-new-buffer-and-set-start-and-end)
   (insert-buffer-substring buffer start end)
   (setq newmark (point)))

insert-buffer-substring 从指定的 bufferstartendregion 中copy文本到当前的 buffer 。 然后,在被插入的文件的最后的位置,保存该 pointnewmark 中。

insert-buffer 的新 body

在 GNU Emacs 22 版中它比原来的更让人迷惑

(push-mark
 (save-excursion
   (insert-buffer-substring (get-buffer buffer))
   (point)))
nil

save-excursion 的返回值是最后一个 expression 的返回值。

beginning-of-buffer 的完整定义

如果没有带 argument 来调用它,则将光标移动到整个 buffer 的开头。如果带有一个 1~10argument ,则会将光标移动到整个buffer 的 argument/10 的位置。如果大于10,再是到 buffer 的最后。 比如,想移动到整个 buffer70% 的位置: C-u 7 M-<

可选 argument

除非告知,否则的话, Lisp 期待一个带有 argumentfunction 在调用时要传递该 argument ,如果不这样子的话,你就收到一条错误消息: "Wrong number of arguments" .

不过,*optional argument* 是 Lisp 的一个特性,可以用特别的关键字来表明一个 argumentoptional 的。这个关键字就是 &optional (注意,*&* 是关键字的一部分)。 如果一个 argument 是跟在 &optional 后面的话,则在调用该 function 时可不必传递该参数。

因此,它的第一行代码是这样子:

(defun beginning-of-buffer (&optional arg)

框架上,整个 function 看起来像这样子:

  (defun beginning-of-buffer (&optional arg)
"documentation…"
(interactive "P")
(or (is-the-argument-a-cons-cell arg)
    (and are-both-transient-mark-mode-and-mark-active-true)
    (push-mark))
(let (determine-size-and-set-it)
(goto-char
  (if-there-is-an-argument
      figure-out-where-to-go
    else-go-to
    (point-min))))
 do-nicety

它与 simplified-beginning-of-buffer 相似,除了 interactive 有 "P" 选项和 goto-char 是在 if-then-else 后面的。

"P" 表示接受一个 prefix argument

goto-char 中有一个 if-then-else 来判断是否传递了 argument ,没有的话,就跳转到 point-min 中。

beginning-of-buffer 带有 argument

如果传递了一个 argument ,下面是完整的 if 部分:

(if (> (buffer-size) 10000)
    ;; Avoid overflow for large buffer sizes!
    (* (prefix-numeric-value arg)
       (/ size 10))
  (/
   (+ 10
      (*
       size (prefix-numeric-value arg))) 10)))
分解 beginning-of-buffer

像其他复杂的 function 一样, 上面的条件语句可以分解为下面的模板 :

(if (buffer-is-large
    divide-buffer-size-by-10-and-multiply-by-arg
  else-use-alternate-calculation

首先检查 buffer 的大小。这是因为,在旧的 Emacs 18 中,能使用的 number 或后面的计算中,不能大于 800W ,如果 buffer 是非常大的话,程序员会强制 Emacs 去尝试超大的数字导致溢出。 术语 overflow ,意味着数字是超级大的。

这里有两种情况: buffer 是非常大的,或者比较小。

  • 非常大的 buffer 的情况

    在以前,是使用 buffer-size 而不是 (> size 10000) , 不仅是因为这这个 function 要调用多次,更因为它应该是不可访问的部分。

    如果符合该条件,则它会 evaluate 下面的 expression

    (*
     (prefix-numeric-value arg)
     (/ size 10))
    

    因为在 interactive 里使用了 P 选项,它表示传递一个 raw prefix argument ,而不是一个 number 。为了执行算术操作,需要进行转换,这就是 prefix-numeric-value 要的事。

  • 在小的 buffer 的情况
    (/ (+ 10 (* size (prefix-numeric-value arg))) 10))
    

回顾

  • or
  • and
  • &optional
  • prefix-numeric-value
  • forward-line
  • erase-buffer
  • bufferp

Narrowing 和 Widening

Narrowing 可以使你专注于 buffer 的特定部分,而不会偶然修改了其他的部分。

Narrowing 的优点

在 Narrowing 中,*buffer* 剩余的其他部分是不可见的,就像它不存在一样。例如,你仅想在 buffer 的一部分中进行单词替换时,narrowing 就有个好处了,它可以允许你仅替换 narrowing 的部分,而不包括其他部分。搜索也同样如此。

由于 narrowing 会使 buffer 的剩余部分不可见,这可能会使一些人感到害怕,他们会认为已经删除了文件的这些部分,然后调用 undo 命令,但并没有关闭 narrowing , 因此,人们可能变得绝望了,如果不知道让这些部分变得可见是通过 widen (通常绑定以 C-x n w )来返回可见性的话的。

save-restriction 特殊的 form

你可以使用 save-restriction 这个特殊的 form 来跟踪 narrowing 中做了些什么。当 Lisp 解析器遇到 save-restriction 时,它会执行 save-restriction 的 body 代码,然后撤消这些代码导致的所有更改。 save-restriction 的模板如下:

(save-restriction
  body… )

body 部分会被 Lisp 解析器按顺序地进行 evaluate

如果你同时连着使用 save-excursionsave-restriction ,你应该在最外面使用 save-excursion 。如果弄反了顺序的话,你可能会导致记录 narrowing 失败。所以,当它们一起用时,模板应该是这样子:

(save-excursion
  (save-restriction
    body…))

其他情况下,当并不是连着用时,它们的顺序应该为: save-restriction 再到 save-excursion

(save-restriction
 (widen)
 (save-excursion
 body…))

what-line

它告诉你当前光标在哪一行上。源码如下:

 (defun what-line ()
"Print the current line number (in the buffer) of point."
(interactive)
(save-restriction
  (widen)
  (save-excursion
    (beginning-of-line)
    (message "Line %d"
             (1+ (count-lines 1 (point)))))))

这个版本是比较旧的代码,最近版本的 GNU Emacs 则不太同,你可以通过 C-h f 来查看。

count-lines 它返回的是在当前行之前一共有多少行。所以这里要 +1

基本的 Function: car, cdr, cons

cons
它用于构建 (construct) list
car 和 cdr
用于获取list的部分元素

奇怪的名字

cons
它是单词 construct 的缩写。
car
这是下面的首字母缩写: Contents of the Address part of the Register
cdr
这是下面的首字母缩写:*Contents of the Decrement part of the Register*

这些用语是在早期开发 Lisp 的计算机硬件的一部分的。所以一直沿用至今。

car 和 cdr

car list 是非常简单的,即获取list的第一个元素。因此 ,*car* 这个 list: (rose violet daisy buttercup) 的结果就是 rose .

(car '(rose violet daisy buttercup))

所以,一个更合理的 car function 的名字,应该是叫 first 。它并不会移除list的第一个元素,仅仅是报告第一个元素是什么。用术语来说,*car* 是非破坏性的。

cdr list 则是list的剩余部分(即除了第一个元素之外的部分),因此 cdr 这个 list:*(rose violet daisy buttercup)* 的结果就是 (violet daisy buttercup)

(cdr '(rose violet daisy buttercup))

同样地,*cdr* 也是不具破坏性的。一个更合理的 cdr 的名字,应该为 rest .

所以说, 一个好的 function 名字是非常重要的。

cons

它用来构建 list 。它与 carcdr 是相反的。例如下面:

(cons 'pine '(fir oak maple))

它会得到一个 list :

(pine fir oak maple)

它不会影响已经存在的 list ,而是创建一个新的 list .

构建一个 list

cons 必须要有一个 list 附于它。你不能以没有任何东西就开始它。如果你想构建一个 list ,则开头至少要有一个空 list 。下面是一些例子:

(cons 'buttercup ())(buttercup)

(cons 'daisy '(buttercup))(daisy buttercup)

(cons 'violet '(daisy buttercup))(violet daisy buttercup)

(cons 'rose '(violet daisy buttercup))(rose violet daisy buttercup)

list 的长度: length

你可以获取 list 有多少个元素,通过 Lisp 的 length function 。例如:

(length '(buttercup))1

(length '(daisy buttercup))2

(length (cons 'violet '(daisy buttercup)))3

你也可以传一个空 list:

(length ())0

nthcdr

nthcdrcdr 是相关联的。它做的事就是重复 ncdr 的操作。下面是一系列的 cdr 操作:

(cdr '(pine fir oak maple))(fir oak maple)

(cdr '(fir oak maple))(oak maple)

(cdr '(oak maple))(maple)

(cdr '(maple))
⇒ nil

(cdr 'nil)
⇒ nil

(cdr ())
⇒ nil

你也可以通过 nthcdr 来直接指定进行重复 n 次的 cdr 操作。例如:

;; Leave the list as it was.
(nthcdr 0 '(pine fir oak maple))(pine fir oak maple)

;; Return a copy without the first element.
(nthcdr 1 '(pine fir oak maple))(fir oak maple)

;; Return a copy of the list without three elements.
(nthcdr 3 '(pine fir oak maple))(maple)

;; Return a copy lacking all four elements.
(nthcdr 4 '(pine fir oak maple))
⇒ nil

;; Return a copy lacking all elements.
(nthcdr 5 '(pine fir oak maple))
⇒ nil

nth

nthnthcdr 返回的结果进行 car 操作。它返回的是第 N 个元素。下面是 nth 可能的代码(实际上,它是用 C 写的,这里只是显示个 lisp 版本的):

(defun nth (n list)
  "Returns the Nth element of LIST.
N counts from zero.  If LIST is not that long, nil is returned."
  (car (nthcdr n list)))

下面是一些例子:

(nth 0 '("one" "two" "three"))"one"

(nth 1 '("one" "two" "three"))"two"

setcar

正如从名字猜测他们的意思一样, setcarsetcdr 会将 carcdr 部分的 list 设置为一个新的 value 。它不像 carcdr ,它会改变原来的 list 的。

首先,我们用创建一个 list ,例如:

(setq animals '(antelope giraffe lion tiger)) 

(setcar animals 'hippopotamus) 

animals

这时,*animals* 为 (hippopotamus giraffe lion tiger) 了。

setcdr

它与 setcar 类似,但它是代替 list 的除了第一个元素的后面的部分的。例如:

(setq domesticated-animals '(horse cow sheep goat))

domesticated-animals => (horse cow sheep goat)

(setcdr domesticated-animals '(cat dog)) 

domesticated-animals => (horse cat dog)

剪切(cut)和保存(store)文本

在 GNU Emacs 中,当你使用 kill command 来 cut 或 clip 文本时,它会保持到一个 list 中,然后你可以使用 yank command 返回这些文本。

在 Emacs 中,单词 kill 处理的文本,它其实并不是摧毁这些 value ,这是由于历史原因才使用这个单词的。一个更合适的单词应该是使用 clip ,这就是 kill 所做的事。从 bufferclip 文本到一个存储的地方,然后也可以从这个地方返回这些文本。

store 文本到一个 list 中

当从 buffer 中 cut 掉一段文本时,它其实是保存在一个 list 中。看起来像这样子:

("a piece of text" "previous piece")

cons 可以用来创建一个新的 list 。像这样子:

(cons "another piece"
  '("a piece of text" "previous piece"))

如果你 evaluate 上面的例子,你可以在 echo area 显示这些元素:

("another piece" "a piece of text" "previous piece")

通过 carnthcdr ,你可以从 list 中获取任意的文本片段。例如:

(car (nthcdr 1 '("another piece"
             "a piece of text"
             "previous piece")))"a piece of text"

zap-to-char

zap-to-char 的完整实现

zap-to-char 会删除当前光标到下一个出现指定字符(包含)之间的 region 。被删除掉的文本,会放到 kill ring 中,然后可以通过 C-y (即 yank)来返回这些文本。如果给定一个 argument ,则它会删除到第N个指定个出现的地方。因此,如果当前光标是在行首,然后指定的字符为 's' ,那么 Thus 会被整个删除。如果 argument 是 2,则 "Thus, if the curs" 会删除到(包括s)"Thus , if the curs" 中 'cursor' 的 's' 处。

如果不指定字符,则不会删除任意文本,并会报告 "Search failed" 。 以下是完整的实现代码(基于 GNU Emacs 22):

(defun zap-to-char (arg char)
  "Kill up to and including ARG'th occurrence of CHAR.
Case is ignored if `case-fold-search' is non-nil in the current buffer.
Goes backward if ARG is negative; error if CHAR not found."
  (interactive "p\ncZap to char: ")
  (if (char-table-p translation-table-for-input)
      (setq char (or (aref translation-table-for-input char) char)))
  (kill-region (point) (progn
                         (search-forward (char-to-string char)
                                         nil nil arg)
                         (point))))
interactive 部分
(interactive "p\ncZap to char: ")

"p" 选项表示接收一个 prefix argument 。因为选项之间是通过换行 \n 来分隔的。第二部分为 "cZap to char: " ,"c" 表示期待一个提示框输入并接受的是一个字符。

在一个 read-only 的 buffer 中,*zap-to-buffer* 会 copy 这些文本到 kill ring 而不会移除它。

zap-to-char 的 body 部分
(if (char-table-p translation-table-for-input)
    (setq char (or (aref translation-table-for-input char) char)))
(kill-region (point) (progn
                       (search-forward (char-to-string char) nil nil arg)
                       (point)))

char-table-p 是一个到目前为止还没有见过的 function 。它会判断 argument 是否是一个 character table 如果是的话,则将它们之一传递到 zap-to-char 中(这对非欧语言来说是非常重要的)。*aref* 会从一个 array 取选出一个元素。

学完 search-foward 后就容易理解 progn 了,所以先看下 search-foward

search-forward function
(search-forward (char-to-string char) nil nil arg)

char-to-string 将 char 转换为 string ,因为 search-forward 要的是 string . 可以看到,它有4个 argument

  1. 第一个是 target ,它必须是 string ,例如: "z" 。Lisp 解析器可能会对 single characterstring of character 有不同的对待。在计算机方面,*single character* 与 string of character 会有不同的电子格式。(single character 经常能被记录为一个字节,命令是 string 可能会用更多的字节,并且计算机需要为这做准备)
  2. 第二个是指定一个 bufferposition 。在这个例子里,允许它搜索到 buffer 结尾,所以,这里为 nil
  3. 第三个是当搜索失败时,它应该做些什么。可以产生一些错误信息或为 nil 。如果为 nil ,则会产生一个错误信号。
  4. 第四年是统计要查找的 string 出现的次数。这是可选的。如果不传的话,那么它就为 1 。如果是负数,则从后面开始搜索。

它的模板为:

(search-forward "target-string"
           limit-of-search
           what-to-do-if-search-fails
           repeat-count)
特殊的 Form : progn

progn form,它会将它的所有 argument 按顺序进行 evaluate ,然后返回最后一个 expression 的 value 。主要用来执行它们,以产生 side effect 的。

它的模板非常简单:

(progn
  body…)

zap-to-char 中,*progn* 做2件事:将光标放到正确的位置;返回光标的位置,以便 kill-region 知道要 kill 到哪里。

progn 中第一个 argument 为 search-forward ,当 search-foward 查找 string 时,它会立即将光标放到 target string 的最后一个字符后面。如果是向后搜索的话,*search-foward* 就会将光标放到 target 的第一个字符中。光标的移动,就一个 side effect

第二个也是最后一个 argument 是 (point) 。它返回光标的位置,它是 search-forward 后的光标的位置。然后, prognpoint 的 value 返回,然后再传递到 kill-region 作为 kill-region 的第二个 argument 。

List 是如何实现的

在 Lisp 中,*atoms* 是以直接的方式来记录的,如果在实现上并不是直接的,尽管如此,在理论上也是直接的。例如一个 atom : rose ,它是记录为四个相邻的字母: 'r', 'o', 's', 'e' 。 在另一方面,一个 List ,是与 atom 不同的。它的机制同样也是简单的,但需要一些时间来适应这种思想。一个 list ,是用一系列的 pointer 来跟踪使用的。在这一系列 pointer 中,每一对 pointer 的第一个 pointer 是指向一个 atom 或者另一个 list ,每一对 pointer 的第二个 pointer 是指向下一对的 pointer ,或者指向 symbol nil ,这表示达到了 list 的结尾。

一个 pointer 自身只是简单地指向它的 electronic address 。因此,一个 list 是持有一系列的 electronic address

List 的示意图

例如,一个 list (rose violet buttercup) 有三个元素, 'rose', 'violet', 'buttercup' 。在计算机里,'rose' 的 electronic address 就是用一段(segment)计算机 memoryaddress 来记录了 atom violet 所在的 electronic address 。同样地, violetelectronic address 记录了 atom buttercup 所在的 electronic address . 我们用个示意图来更明了:

___ ___      ___ ___      ___ ___
|___|___|--> |___|___|--> |___|___|--> nil
  |            |            |
  |            |            |
  --> rose     --> violet   --> buttercup

在示意图中,每一个 box 代表一个计算机 memory 持有的 Lisp Object 的单词,通常是一个 memory address 。Boxes ,例如,该 addresses 是成对出现的。每一个箭头指向下一个 address ,该 address 可以为 atom 或另一对 address 。第一个 box 就是 'rose' 的 electronic address ,并且箭头指向 'rose' ;第二个 box 是下一对 boxes 的 address 。如果最后一个 box 是指向 nil 则表示结束。

当使用 setq 这样子的 function 来设置一个 variable 时,它保存了第一个 box 的 addressvariable 中。因此, evaluate 下面的 expression 会创建这样的情况:

(setq bouquet '(rose violet buttercup))
bouquet
  |
  |     ___ ___      ___ ___      ___ ___
   --> |___|___|--> |___|___|--> |___|___|--> nil
         |            |            |
         |            |            |
          --> rose     --> violet   --> buttercup

在这个例子里,symbol bouquet 拥有第一对 boxes 的 address 。同样的 list ,可以用不种类型的 box 来加以说明,比如这:

bouquet
 |
 |    --------------       ---------------       ----------------
 |   | car   | cdr  |     | car    | cdr  |     | car     | cdr  |
  -->| rose  |   o------->| violet |   o------->| butter- |  nil |
     |       |      |     |        |      |     | cup     |      |
      --------------       ---------------       ----------------

如果一个 symbol 是由 cdr 设置的话,list 自身并不会导致改变;该 symbol 只是简单地拥有list剩余的部分的 address 。因此 evaluate 下面的 expression ,

(setq flowers (cdr bouquet))

会产生:

bouquet        flowers
  |              |
  |     ___ ___  |     ___ ___      ___ ___
  --> |   |   |  --> |   |   |    |   |   |
      |___|___|----> |___|___|--> |___|___|--> nil
        |              |            |
        |              |            |
        --> rose       --> violet   --> buttercup

一对 boxes 的 address 就称为一个 cons celldotted pair 。*cons* function 会添加一对新的 addresses 到这一系列的 addresses 前面。例如,evaluate 下面的 expression:

(setq bouquet (cons 'lily bouquet))

会产生:

bouquet                       flowers
  |                             |
  |    ___ ___        ___ ___  |     ___ ___       ___ ___
  --> |   |   |      |   |   |  --> |   |   |     |   |   |
      |___|___|----> |___|___|----> |___|___|---->|___|___|--> nil
        |              |              |             |
        |              |              |             |
        --> lily      --> rose       --> violet    --> buttercup

然而,这并不会改变 symbol flowers 的 value ,例如 :

(eq (cdr (cdr bouquet)) flowers)

它会返回 t ,表示 true.

除非 reset ,否则 flowers 会一直拥有 (violet buttercup) 的 value 。

因此,在 Lisp 中,当你 cdr 一个 list 时,你只是获取了下一个 cons celladdresscar 一个 list 时,你只是获取了 list 中第一个元素的 address 。 使用 cons 添加一个新的元素时,你只是添加了一个新的 cons cell 到 list 的前面。

这就是它们操作的全部!

symbol 作为抽屉柜

在之前部分里,我建议你把 symbol 想象为一个抽屉柜。function 定义放在其中一个,value 放在另一个等等。放到抽屉里,持有的 value 的抽屉可以在不影响持有 function 定义抽屉的内容下被更改。反之亦然。 实际上,每个放到抽屉里的是 value 或 function 定义的 address 。就像你在一个旧的楼阁里的柜子中,在其中一个抽屉里,你发现了一张藏宝图一样。

除了 name ,symbol 的定义 和 variable 的 value 之外,*symbol* 还有一个 property list 的抽屉,它用于记录其他的信息。下面是一张假设图:

    Chest of Drawers            Contents of Drawers

    __   o0O0o   __
  /                 \
 ---------------------
|    directions to    |            [map to]
|     symbol name     |             bouquet
|                     |
+---------------------+
|    directions to    |
|  symbol definition  |             [none]
|                     |
+---------------------+
|    directions to    |            [map to]
|    variable value   |             (rose violet buttercup)
|                     |
+---------------------+
|    directions to    |
|    property list    |             [not described here]
|                     |
+---------------------+
|/                   \|

Yanking 回文本

无论什么时候你从 buffer 中使用 kill command 来 cut 掉文本时,你都可以用 yank 命令来将它们返回。这些被 kill 掉的文本,它们被放到一个 kill ring 里,*yank* command 则从 kill ring 里插入适当的内容到一个 buffer 中(不必是原来的那个 buffer )

简单的 C-y (yank) 会从 kill ring 中插入第一个项到当前的 buffer 中。如果 C-y command 是紧接着 M-y 的话,则第一个元素会被第二个元素替代。连续的 M-y 的话,会被第二个,第三个,第四个等等的元素替代。当达到了 kill ring 的最后一个元素的时候,它会被第一个元素替代然后循环重复。(这就是为什么 kill ring 之所以叫 ring 而不仅是一个 list )。然而实际的数据结构拥有文本的是一个 list 。

Kill Ring 概述

kill ring 是一个文本 string 的 list。看起来像这样子:

("some text" "a different piece of text" "yet more text")

如果你的 kill ring 是这些内容的话,并且我按下 C-y ,则字符串 "some text" 会插入到当前光标所在的位置的 buffer 中。 yank command 也用于 copy 中的文本,被 copy 的文本并不会从 buffer 中 cut 掉,但是 copy 的文本也会放到 kill ring 中,然后通过 yank 插入回来。

yank (C-y) 用于从 kill ring 中返回文本。 yank-pop (M-y) ,从 kill ring 中弹出第一个元素(即所谓第二个元素,替代了第一个元素) rotate-yank-pointer ,通常是上面两个来调用的。

引用到 kill ring 的 functions 都是通过一个称为 kill-ring-yank-pointer 的 variable 来处理的。的确,从 yankyank-pop functions 中插入的源码为 :

(insert (car kill-ring-yank-pointer))

不过,在 GNU Emacs 22 中就不再是这样子了。这个 function 已经被 insert-for-yank 替代了 。

kill-ring-yank-pointer variable

就像 kill-ring 作为一个 variable 一样。如果有一个 kill ring 的 value 为:

("some text" "a different piece of text" "yet more text")

并且 kill-ring-yank-pointer 指向第二句的话,则 kill-ring-yank-pointer 的 value 为:

("a different piece of text" "yet more text")

正如之前解释的一样,计算机并不会保持两分这些 text 的 copy (kill-ring 和 kill-ring-yank-pointer) ,而是有两个 Lisp variable ,它们都指向同一份文本。这是示意图:

kill-ring     kill-ring-yank-pointer
 |               |
 |      ___ ___  |     ___ ___      ___ ___
  ---> |   |   |  --> |   |   |    |   |   |
       |___|___|----> |___|___|--> |___|___|--> nil
         |              |            |
         |              |            |
         |              |             --> "yet more text"
         |              |
         |               --> "a different piece of text"
         |
          --> "some text"

kill-ringkill-ring-yank-pointer 两个 variable 都是 pointer 。但是 kill ring 自身才是我们实际上指的 kill ring 。*kill-ring* 我们通常指的是它自身 kill ring ,而不是一个指向 list 的 pointer 。相反地, kill-ring-yank-pointer 通常是作为一个 pointer 来指向一个 list .

kill-ring 通常想像为一个完整的数据结构,而 kill-ring-yank-pointer 则是作为一个 pointer 指向将会被插入的元素的位置。

loops 和 recursion

while

while 中,如果第一个 argument 返回的是 false, 则 Lisp 解析器会忽略之后的 expression (即 while 的 body 部分)。如果返回的是 true ,则它会 evaluate 它的 body 部分,然后再一次判断它的第一个 argument 是否是 true 还是 false 。如果又是 true ,则又会进行 evaluate 。它的模板如下:

(while true-or-false-test
       body…)

用 while 来进行 loop

注意, while 自身的 expression 永不会返回一个 true 的 value 的。这意味着, while 总是用作 side effect 的 。

一个 while loop 和一个 List

一个常用控制 while 的 loop 是来用 test 一个 list 是否有元素。如果有,则重复 loop ,否则结束循环。 一个简单 test 一个 list 是否有元素的方式是进行 evaluate 一个 list 。如果没有元素,则是一个空 list ,并且会返回一个空 list ,这是 nilfalse 的同义词。 换句话说,一个带有元素的 list ,当它进行 evaluate 的时候,它会返回这些元素 。由于 Emacs Lisp 会认为所有非 nil 的 value 都为 true ,所以一个返回元素的的 list ,在 while 的 test 中会返回 true

例如设置一个空 list:

(setq empty-list ())

empty-list

然后 evaluate 这个 empty-list 时,可以在 echo area 中显示一个 nil

但如果是含有元素的话:

(setq animals '(gazelle giraffe lion tiger))

animals

这时可以用使用 while :

(while animals
   …

为了避免它一直循环,需要有一个提供空 list 的机制 。例如:

(setq animals (cdr animals))

所以,一个框架如下:

(while test-whether-list-is-empty
  body…
  set-list-to-cdr-of-list)

例子: print-elements-of-list

(setq animals '(gazelle giraffe lion tiger))

(defun print-elements-of-list (list)
  "Print each element of LIST on a line of its own."
  (while list
    (print (car list))
    (setq list (cdr list))))

(print-elements-of-list animals)

带有增量计数器的 Loop

除了通过 list 来控制 loop 之个,一个常见的方式是使用 counter 。

增量 Loop 的细节

带有计数器的 loop 它有一个 expression ,例如: (< count desired-number) 。下面是一个模板:

set-count-to-initial-value
  (while (< count desired-number)         ; true-or-false-test
         body…
         (setq count (1+ count)))              ; incrementer
例如用计数器的 loop 来统计星星数:
   *
  * *
 * * *
* * * *

框架:

(let ((total 0)
     (row-number 1))
     body…)

(<= row-number number-of-rows)

(setq total (+ total row-number))

(setq row-number (1+ row-number))
完整代码
(defun triangle (number-of-rows)    ; Version with
                                        ;   incrementing counter.
  "Add up the number of pebbles in a triangle.
The first row has one pebble, the second row two pebbles,
the third row three pebbles, and so on.
The argument is NUMBER-OF-ROWS."
  (let ((total 0)
        (row-number 1))
    (while (<= row-number number-of-rows)
      (setq total (+ total row-number))
      (setq row-number (1+ row-number)))
    total))

    (triangle 4)

带有减量计数器的 Loop

模板:

(while (> counter 0)                    ; true-or-false-test
    body…
    (setq counter (1- counter)))          ; decrementer

以下是完整的用减量计数器的版本:

(defun triangle (number)                ; Second version.
  "Return sum of numbers 1 through NUMBER inclusive."
  (let ((total 0))
    (while (> number 0)
      (setq total (+ total number))
      (setq number (1- number)))
    total))

recursion

构建 robots

有时,作一个任务时,想像为一个 robot 来执行这些程序是有帮助的。为了干这个工作,一个 recursion function 调用第二个 robot 来帮助它。这个第二个的 robot 做的同第一个做的是工作指令是一样的,除了第二个 robot 的 argument 跟第一个的 robot 有所不同之外,其他是一样的。

在一个 recursion function 中,第二个 robot 可以调用第三个; 第三个 robot 可能调用第四个,等等。直到最后一个 robot ,它应该知道什么时候该停止。

有一点是非常重要的:就是要将不同的 argument 传递到下一个 robot ,否则的话会导致一直不会停下来的。

recursion 定义部分

一个 recursion function 典型地有一个带有三部分的条件 expression:

  1. 一个 true-or-false-test ,判断该 function 是否要再次调用,这里称为 do-again-test
  2. function 的名字。当调用时,一个新的 function instance 即一个新的 robot - 会被创建以及被告知要做什么。
  3. 当 function 调用时,有一个 expression ,第在每次调用 function 时,都会返回不同的 value ,这里称为 next-step-expression

它的模板为:

(defun name-of-recursive-function (argument-list)
"documentation…"
(if do-again-test
  body…
  (name-of-recursive-function
       next-step-expression)))
使用 recursion 来处理 list
(setq animals '(gazelle giraffe lion tiger))

(defun print-elements-recursively (list)
  "Print each element of LIST on a line of its own.
Uses recursion."
  (when list                            ; do-again-test
    (print (car list))              ; body
    (print-elements-recursively     ; recursive call
     (cdr list))))                  ; next-step-expression

(print-elements-recursively animals)
使用 recursion 来替代 counter
(defun triangle-recursively (number)
  "Return the sum of the numbers 1 through NUMBER inclusive.
Uses recursion."
  (if (= number 1)                    ; do-again-test
      1                               ; then-part
    (+ number                         ; else-part
       (triangle-recursively          ; recursive call
        (1- number)))))               ; next-step-expression

(triangle-recursively 7)

regular expression 搜索

有2个 function 就是使用这种搜索的: foward-sentenceforward-paragraph 。*regular expression* 通常写作 regexp

sentence-end 的 regular expression

symbol sentence-end 是绑定到一个 pattern ,它是标记 sentence 结束的。 显然,一个 sentence 应该是以句号,或问号或叹号结尾的。这意味着这个 pattern 应该包括这些字符集:

[.?!]

然而,我们并不想 forward-sentence 仅仅跳到一个句号,或一个问号或一个叹号处,因为这些字符可以在 sentence 的中间使用。比如句号,可以用在缩写字符后。所以这里需要其他额外的信息。 根据惯例,你会在每个 sentence 后面输入2个空格,仅在句号或问号或叹号后面有一个空格。所以,这些字符后跟着2个空格的就可以指明是一个 sentence 了。然而在文件中,2个空格可以用 tab 或换行来表示结束。所以,这个 regular expression 可以为修改为:

\\($\\| \\|  \\)
       ^   ^^
      TAB  SPC

在这里, "$" 表示行尾。

2条斜线,=\\= 在括号和垂直线之前是必须的:第一条斜线引用第二条斜线;并且第二条斜线显示接下来的字符,括号或垂直线是特殊字符。一个 sentence 也可能跟在一个或多个中括号,例如:

[
]*

tabs 和 空格 , 一个中括号可以直接写字面值到 regular expression 中。星号表示重复0或多次。 但一个 sentence 不仅是以以上字符结尾的,也可能是引号 " 或一个关闭的大括号 } 。所以 regexp 为:

[]\"')}]*

所以,完整的 pattern 为:

sentence-end
  ⇒ "[.?!][]\"')}]*\\($\\|     \\|  \\)[
]*"

re-search-forward function

它与 search-foward 是非常相似的。当搜索成功时,它会立即离开当前光标位置到 target 的最后一个字符后面。如果是向 backward 搜索的话,则是放到 target 的第一个字符的前面。与 search-foward 类似,*re-search-foward* 有4个 argument :

  1. 第一个为 regular expression 用来搜索的。它是一个用双引号引起的 string
  2. 可选的第二个 argument ,表示限制该搜索可以搜索多远。即 buffer 的 position
  3. 可选的第三个 argument ,表示如果搜索失败时,要做些什么。*nil* 的话,表示导致一个错误信号并打印错误消息。
  4. 可选的第四个 argument ,表示重复的次数。如果为负,则表示是向 backward 搜索 。

模板如下:

(re-search-forward "regular-expression"
             limit-of-search
             what-to-do-if-search-fails
             repeat-count)

forward-sentence

通常,它绑定到 M-e 快捷键中。

完整代码

(defun forward-sentence (&optional arg)
  "Move forward to next end of sentence.  With argument, repeat.
With negative argument, move backward repeatedly to start of sentence.

The variable `sentence-end' is a regular expression that matches ends of
sentences.  Also, every paragraph boundary terminates sentences as well."
  (interactive "p")
  (or arg (setq arg 1))
  (let ((opoint (point))
        (sentence-end (sentence-end)))
    (while (< arg 0)
      (let ((pos (point))
            (par-beg (save-excursion (start-of-paragraph-text) (point))))
        (if (and (re-search-backward sentence-end par-beg t)
                 (or (< (match-end 0) pos)
                     (re-search-backward sentence-end par-beg t)))
            (goto-char (match-end 0))
          (goto-char par-beg)))
      (setq arg (1+ arg)))
    (while (> arg 0)
      (let ((par-end (save-excursion (end-of-paragraph-text) (point))))
        (if (re-search-forward sentence-end par-end t)
            (skip-chars-backward " \t\n")
          (goto-char par-end)))
      (setq arg (1- arg)))
    (constrain-to-field nil opoint t)))

最好先看骨架,再看"肌肉" 。骨架如下:

(defun forward-sentence (&optional arg)
  "documentation…"
  (interactive "p")
  (or arg (setq arg 1))
  (let ((opoint (point)) (sentence-end (sentence-end)))
    (while (< arg 0)
      (let ((pos (point))
            (par-beg (save-excursion (start-of-paragraph-text) (point))))
       rest-of-body-of-while-loop-when-going-backwards
    (while (> arg 0)
      (let ((par-end (save-excursion (end-of-paragraph-text) (point))))
       rest-of-body-of-while-loop-when-going-forwards
    handle-forms-and-equivalent

forward-paragraph

它也是用 regexp 来处理的。

通过 regexp 和 累计 来统计单词

Counting words

标准的 Emacs 发行版里已经有在一个 region 中统计行和单词的 functions 了。*count-words-region*

count-words-example function

设计 count-words-example

模板:

(defun name-of-function (argument-list)
    "documentation…"
    (interactive-expression…)
  body…)

我们需要做的,就是填充这些槽。

单词的 regular expression 为:

\w+\W*

(re-search-forward "\\w+\\W*")

然后累加:

(setq count (1+ count))

因此就有了下面的 function 定义:

     ;;; First version; has bugs!
(defun count-words-example (beginning end)
  "Print number of words in the region.
Words are defined as at least one word-constituent
character followed by at least one character that
is not a word-constituent.  The buffer's syntax
table determines which characters these are."
  (interactive "r")
  (message "Counting words in region ... ")

;;; 1. Set up appropriate conditions.
  (save-excursion
    (goto-char beginning)
    (let ((count 0))

;;; 2. Run the while loop.
      (while (< (point) end)
        (re-search-forward "\\w+\\W*")
        (setq count (1+ count)))

;;; 3. Send a message to the user.
      (cond ((zerop count)
             (message
              "The region does NOT have any words."))
            ((= 1 count)
             (message
              "The region has 1 word."))
            (t
             (message
              "The region has %d words." count))))))
空格的 bug

首先,如果你在一个仅有空格的 region 中,调用这个 function,它会告诉你这个 region 有一个单词! 其次,如果你标记的 region 在 buffer 的结尾仅有空格字符或 访问一个 narrowing 的 buffer ,它会显示一个错误信息:

Search failed: "\\w+\\W*"

比如有下面的内容:

one   two   three

当你从行首的开始 mark 到 "one* 单词之前的这一段空格 region ,然后调用这个 function 时,Emacs 会告诉你,这有一个单词!(bug).

然后将光标放到 "three" 后面,标记这个位置到 buffer 的结尾,这时再调用的话,Emacs 会产生一条错误信息:"Search failed"

这两个 bug 来自同一个问题: 这个搜索试图扩展搜索的 region 。解决办法就是限制搜索的 region

完善版:

     ;;; Final version: while
(defun count-words-example (beginning end)
  "Print number of words in the region."
  (interactive "r")
  (message "Counting words in region ... ")

;;; 1. Set up appropriate conditions.
  (save-excursion
    (let ((count 0))
      (goto-char beginning)

;;; 2. Run the while loop.
      (while (and (< (point) end)
                  (re-search-forward "\\w+\\W*" end t))
        (setq count (1+ count)))

;;; 3. Send a message to the user.
      (cond ((zerop count)
             (message
              "The region does NOT have any words."))
            ((= 1 count)
             (message
              "The region has 1 word."))
            (t
             (message
              "The region has %d words." count))))))

你的 .emacs 文件

Emacs 的默认配置

默认情况下,它会根据你的文件后缀来打开相应的 mode ,如果找不到的话,就会使用 Fundamental mode

初始化文件

Emacs 会自动加载这些 site-wide 初始化文件的,你的 .emacs 文件是仅为你加载的,但这个是加所有人都会加载的。

这里有2个 site-wide 的初始化文件: site-load.elsite-init.el 。(编译和构建 Emacs 时使用)

当你每次启动 Emacs ,有3个其他的 site-wide 初始化文件会自动地被加载,如果存在的话。

  1. site-start.el 会在你的 .emacs 文件之前加载
  2. default.el 会在你的 .emacs 文件之后加载
  3. terminal type 文件,会在你的 .emacs 文件之后加载

使用 defcustom 来指定 variables

之后,你就可以在 Emacs's 的 customize 功能里设置它们的 value 了。

defcustom (前三个与 defvar 是一样的)

  1. 第一个 argument 为 variable 的 name 。
  2. 第二个为 variable 的初始 value ,并且这个 value 仅在原来没有 value 的情况下才会 set的
  3. 第三部分是 documentation
  4. 第四和剩余部分是指明类型和选项 (可选)。这些 argument 是由一个 keyword 然后接着一个 value 组成的。每个 keyword 是由分号 ":" 开始的。例如:
(defcustom text-mode-hook nil
  "Normal hook run when entering Text mode and many related modes."
  :type 'hook
  :options '(turn-on-auto-fill flyspell-mode)
  :group 'wp)

开始 .emacs 文件

当你启动 Emacs 时,它会加载你的 .emacs 文件,除非你通过 "-q" 选项来告诉它不要加载,

Text 和 Auto fill mode

开始 Text mode 和 *Auto fill mode*:

   ;;; Text mode and Auto Fill mode
;; The next two lines put Emacs into Text mode
;; and Auto Fill mode, and are for writers who
;; want to start writing prose rather than code.
(setq-default major-mode 'text-mode)
(add-hook 'text-mode-hook 'turn-on-auto-fill)

'text-mode 告诉 Emacs 直接处理 text-mode symbol ,无论它代表的是什么。

(add-hook 'text-mode-hook 'turn-on-auto-fill) 表示每次开启 Text mode 的时候,Emacs 都会执行所有与 text-mode-hook 相关的 commands 。

(setq colon-double-space t) 表示在每个分号后都插入2个空格。

mail aliases

   ;;; Mail mode
                                        ; To enter mail mode, type 'C-x m'
                                        ; To enter RMAIL (for reading mail),
                                        ; type 'M-x rmail'
(setq mail-aliases t)

setqmail-aliases 的 value 设置为 t 。表示 true ,即表示: "Yes, use mail aliases"

这里可以在 ~/.mailrc 中加上 aliases 了:内容类似:

alias geo george@foobar.wiz.edu

indent tabs mode

默认情况下,当 format 一个 region 时 Emacs 将会多个空格替换为 tab 。以下是关闭这个mode的代码:

;;; Prevent Extraneous Tabs
(setq-default indent-tabs-mode nil)

注意,这里使用 setq-default 而不是 setq 。*setq-default* 仅在 buffer 中没有他们自己的 local values 时,才设置它的 values .

keybindings

(global-set-key "\C-cw" 'compare-windows)

Keymaps

Emacs 中使用 Keymaps 来记录哪些键绑定了哪些 commands 。当你用 global-set-key 时,会修改 current-global-map 。每一种 mode 都有它们自己的 keymaps 。 mode 指定的 keymap 会覆盖 global map 的。

loading files

你可以使用 load command 来 evaluate 一个完整的文件:

(load "~/emacs/slowsplit")

Autoloading

通过 loadevaluate 是会实际就安装好了的。但 autoloading 则是在第一次调用的时候才会安装的。(这可以让 Emacs 启动得更快)。例如:

(autoload 'html-helper-mode
"html-helper-mode" "Edit HTML documents" t)

autoload 有5个 argument:

  1. function name
  2. 要加载的 fien name
  3. documentation
  4. 是否可以通过 interactively 来调用
  5. 告诉 autoload 什么类型的对象可以处理一个 keymap 或 macro 以及一个 function (默认为 function)

Debugging

debug

debug-on-entry

指定入口来进行 debug M-x debug-on-entry RET function-name RET

脚注:

1

参见维基百科 S-表达式

作者: emacsist

Created: 2017-03-15 Wed 17:12